package biback.domain.common

import biback.domain._
import biback.domain.price.{BaseInt, IntPrice}
import cats.data.NonEmptyList

import scala.collection.immutable.SortedMap

object Calculator {
  def calcInt(price: IntPrice, base: NonEmptyList[BaseInt], balances: SortedMap[TrnDate, BiDec], start: TrnDate, end: TrnDate): BiDec = {
    val segments = base.toList.map(f => (f.effectiveDate, None)) ++ balances.toList.map(f => (f._1, Some(f._2)))
    val sorted = segments.sortBy(_._1).dropWhile(_._2.isEmpty)
      .foldLeft(List.empty[(TrnDate, BiDec)]) { (out, f) =>
        val (dt, bal) = f
        if (out.isEmpty) bal.toList.map((dt, _))
        else out ++ List((dt, bal.getOrElse(out.last._2)))
      }
    val calcAmt: BiDec => BiDec = _.floorX(price.intKey.ccy.minCalcIntScale)
    val result = base.toList.foldRight((sorted, BiDec0, end)) { (f, out) =>
      val (l, r) = out._1.span(_._1.value < f.effectiveDate.value)
      val sum = r.foldRight((out._3, BiDec0))((x, z) => (x._1, z._2 + (z._1 - x._1) * calcAmt(x._2)))
      (l, out._2 + sum._2 * price.dayRate(f), f.effectiveDate)
    }
    result._2.floorX(price.intKey.ccy.minIntScale)
  }
  
  def calcTermPay(capital: BiDec, price: IntPrice, base: BaseInt, term: Int): BiDec = {
    val m = price.monthRate(base)
    val pay = (m * (BiDec1 + m).pow(term) / ((BiDec1 + m).pow(term) - BiDec1)) * capital
    pay.roundX(base.key.ccy.minIntScale)
  }

  case class RepayTerm(leftCapital: BiDec, n: Int, dt: TrnDate, capAmt: BiDec, intAmt: BiDec) {
    implicit val width: Int = 8
    def print() =
      println(s"${n.format(2)} :: $dt = ${leftCapital.format}, ${capAmt.format}, ${intAmt.format}")
  }

  def calcTermPayPlan(capital: BiDec, price: IntPrice, base: BaseInt, term: Int, loanDay: TrnDate): List[RepayTerm] = {
    val rounded = (x: BiDec) => x.roundX(base.key.ccy.minIntScale)
    val m = price.monthRate(base)
    val pay = rounded((m * (BiDec1 + m).pow(term) / ((BiDec1 + m).pow(term) - BiDec1)) * capital)
    val repayDay = loanDay.nearestDay(21).toOption.get
    val monthRate = price.monthRate(base)
    val termPay = calcTermPay(capital, price, base, term)
    val init = List(RepayTerm(rounded(capital), 0, repayDay, rounded(BiDec0), rounded(BiDec0)))
    val plan = (1 until term).foldLeft(init) { (p, n) =>
      val i = rounded(p.last.leftCapital * monthRate)
      val c = termPay - i
      p ++ List(RepayTerm(p.last.leftCapital - c, n, p.last.dt.nextTerm().toOption.get, c, i))
    }
    val firstInt = rounded(capital * (plan(1).dt - loanDay) * price.dayRate(base))
    val repay1 = plan(1).copy(intAmt = firstInt)
    val endDay = loanDay.nextTerm(term).toOption.get
    val lastInt = plan.last.leftCapital * (endDay - plan.last.dt) * price.dayRate(base)
    List(repay1) ++ plan.drop(2) ++ List(RepayTerm(BiDec0, term, endDay, plan.last.leftCapital, rounded(lastInt)))
  }

  def calcDayInt(capital: BiDec, price: IntPrice, base: BaseInt, start: TrnDate, end: TrnDate): BiDec = {
    val i = price.dayRate(base) * (end - start) * capital
    i.roundX(base.key.ccy.minIntScale)
  }

  def calcCapital(leftCapital: BiDec, monthRate: BiDec, termPay: BiDec): BiDec =
    leftCapital - (termPay - leftCapital * monthRate)

  def calcTermCapital(leftCapital: BiDec, monthRate: BiDec, termPay: BiDec, n: Int): BiDec =
    (0 until n).foldLeft(leftCapital)((left, _) => calcCapital(left, monthRate, termPay))

  def calcTermInt(leftCapital: BiDec, monthRate: BiDec, termPay: BiDec, n: Int): BiDec =
    calcTermCapital(leftCapital, monthRate, termPay, n - 1) * monthRate
}
